🎖️GitЯра🎖️
Commit ae7eaa6710c37db551eb68acd09e0c7101af43fa
Parents : 99e8354
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-08-16T07:04:01Z
Committer : GitHub <noreply@github.com>
Date : 2026-08-16T07:04:01Z
fix(network): handle IPv6 mDNS addresses correctly, refresh on re-announce (#6729)
Changes
3 files changed, 46 insertions(+), 6 deletions(-)
Diff
diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/NetworkRepository.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/NetworkRepository.kt
index 19863dcb8f..59d30f834e 100644
--- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/NetworkRepository.kt
+++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/NetworkRepository.kt
@@ -24,7 +24,13 @@ interface NetworkRepository {
companion object {
fun DiscoveredService.toAddressString() = buildString {
- append(hostAddress)
+ // Bracket IPv6 literals (they contain ':') so appending ":$port" below stays
+ // unambiguous — "fe80::1:8080" is not parseable as host+port, "[fe80::1]:8080" is.
+ if (hostAddress.contains(':')) {
+ append('[').append(hostAddress).append(']')
+ } else {
+ append(hostAddress)
+ }
if (port != NetworkConstants.SERVICE_PORT) {
append(":$port")
}
diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/TcpTransport.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/TcpTransport.kt
index 6a4e3b501b..719a08f20b 100644
--- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/TcpTransport.kt
+++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/TcpTransport.kt
@@ -254,9 +254,7 @@ class TcpTransport(
*/
@Suppress("NestedBlockDepth")
private suspend fun connectAndRead(address: String): Boolean = withContext(dispatchers.io) {
- val parts = address.split(":", limit = 2)
- val host = parts[0]
- val port = parts.getOrNull(1)?.toIntOrNull() ?: StreamFrameCodec.DEFAULT_TCP_PORT
+ val (host, port) = parseHostAndPort(address)
Logger.i { "$logTag: [$address] Connecting to $host:$port" }
val attemptStart = nowMillis
@@ -298,6 +296,28 @@ class TcpTransport(
}
}
+ /**
+ * Split a discovered/entered address into host and port. Handles a bracketed IPv6 literal (`[fe80::1]:4403`,
+ * matching [org.meshtastic.core.network.repository.NetworkRepository]'s `toAddressString()`) as well as the plain
+ * `host` / `host:port` forms used for IPv4 and hostnames — a bare `address.split(":", limit = 2)` would misparse an
+ * unbracketed IPv6 literal's multiple colons.
+ */
+ private fun parseHostAndPort(address: String): Pair<String, Int> {
+ if (address.startsWith("[")) {
+ val closeBracket = address.indexOf(']')
+ if (closeBracket != -1) {
+ val host = address.substring(1, closeBracket)
+ val port =
+ address.substring(closeBracket + 1).removePrefix(":").toIntOrNull()
+ ?: StreamFrameCodec.DEFAULT_TCP_PORT
+ return host to port
+ }
+ }
+ val parts = address.split(":", limit = 2)
+ val port = parts.getOrNull(1)?.toIntOrNull() ?: StreamFrameCodec.DEFAULT_TCP_PORT
+ return parts[0] to port
+ }
+
/**
* Read until EOF or [SOCKET_RETRIES] consecutive inactivity timeouts. [withTimeoutOrNull] gives a *resumable*
* inactivity timeout: cancelling a parked `readAvailable` leaves the channel usable for the next iteration
diff --git a/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/repository/JvmServiceDiscovery.kt b/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/repository/JvmServiceDiscovery.kt
index aeef1a42f7..8eb0e01837 100644
--- a/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/repository/JvmServiceDiscovery.kt
+++ b/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/repository/JvmServiceDiscovery.kt
@@ -58,7 +58,10 @@ class JvmServiceDiscovery(private val dispatchers: CoroutineDispatchers) : Servi
val listener =
object : ServiceListener {
override fun serviceAdded(event: ServiceEvent) {
- jmdns?.requestServiceInfo(event.type, event.name)
+ // persistent=true: keep calling serviceResolved as fresh records arrive (re-announcements,
+ // IP changes) instead of only once — otherwise a resolved device's address goes stale until
+ // it's fully removed and rediscovered from scratch.
+ jmdns?.requestServiceInfo(event.type, event.name, true)
}
override fun serviceRemoved(event: ServiceEvent) {
@@ -75,7 +78,18 @@ class JvmServiceDiscovery(private val dispatchers: CoroutineDispatchers) : Servi
val discovered =
DiscoveredService(
name = info.name,
- hostAddress = info.hostAddresses.firstOrNull() ?: "",
+ // Prefer IPv4: jmdns's IPv6 candidates are often link-local
+ // (fe80::/10) addresses that need a zone/scope ID to be
+ // routable, which jmdns doesn't supply. Fall back to a raw
+ // (unbracketed) IPv6 literal via inet6Addresses rather than
+ // jmdns's own hostAddresses array, which already brackets
+ // IPv6 strings ("[fe80::1]") -- toAddressString() below adds
+ // its own brackets, so starting from an already-bracketed
+ // value would double-bracket and break parsing downstream.
+ hostAddress =
+ info.inet4Addresses.firstOrNull()?.hostAddress
+ ?: info.inet6Addresses.firstOrNull()?.hostAddress
+ ?: "",
port = info.port,
txt = txtMap,
)
Served by rngit 1.5.0 - Generated in 0.1s